---
menu: Active Proposals
name: beforefocus/focusNext (Explainer)
layout: ../../layouts/ComponentLayout.astro
---

Authors: [Marat Abdullin](https://github.com/mshoho)

{/* START doctoc generated TOC please keep comment here to allow auto update */}
{/* DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE */}
{/* END doctoc generated TOC please keep comment here to allow auto update */}

## Introduction

Browsers include complex heuristics for determining the next element to focus, but these aren't
exposed to web developers, leaving them to implement these heuristics from scratch, often requiring
DOM modificatons, or writing code that causes reflows and style recomputations. It can be hard or
sometimes impossible to apply in some particular scenarios (for example, you might have control of
a parent list component, but not the contents of the list items).

While having additional new high level abstractions to handle keyboard navigation (such as the
[focusgroup](./focusgroup.explainer)) will be useful, there is a multitude of different
scenarios for focus management, and lower level primitives will give web developers more flexibility.

This proposal focuses on having two fairly simple lower level functions which would arguably allow
the developers to implement whatever patterns they prefer without working around the browser
limitations and impossibilities.

Namely, one method called `document.focusNext()` and one event called `beforefocus`.
Let's dive deeper into it.

## document.focusNext()

`document.focusNext()` method just exposes the default action the browsers have for `Tab` and
`Shift+Tab` presses.

The method's signature should be like:

```ts
focusNext(options?: {
   contextElement?: HTMLElement,
   direction?: FORWARD | BACKWARD,
   trapped?: boolean,
   includeContextElement?: boolean,
});
```

`contextElement` is the starting point from which the browser should start looking for the next
element to focus. It is optional and equals to `document.activeElement` by default.

`direction` is the direction of the search. `FORWARD` to get the `Tab` press behaviour and
`BACKWARD` to get `Shift+Tab` press behaviour. It is also optional and equals to `FORWARD` by
default.

`trapped` means that instead of going to the address bar after the last element, the search
should continue from the beginnig of the page (or from the end of it in case of moving
backwards). False by default.

By setting `includeContextElement` to true, `focusNext()` will also include `contextElement` in the
search for next element to focus (and focus it if it is focusable). False by default.
In some scenarios it is more convenient to start considering elements including `contextElement`,
for example, if you have a group of focusable elements that needs to be skipped, it could be more
convenient to specify next group's container instead of locating last deepest child of the current
group that needs to be skipped.

This method should invoke exactly the same internal routine to locate and focus
elements which the currently existing default action for `Tab` and `Shift+Tab` invokes.

The important expected behaviours:

* If the `contextElement` is the last element in the DOM, calling `document.focusNext()` moves
   focus outside of the page to the browser's address bar — the same thing `Tab` press default
   action does.
* If an `<iframe>` comes after the `contextElement`, calling `document.focusNext()` should
   move focus to the first focusable element inside the iframe (if the iframe has focusable
   elements) or past the iframe (if there is nothing to focus in the iframe) — the same thing
   `Tab` press default action does.
* The same logic applies for mirroring `Shift+Tab` behaviour when `direction` is set ot `BACKWARD`.
* `document.focusNext()` should have no effect if the page is not focused or the focus is in
   the address bar.
* When `trapped` is true, the focus should cycle inside the page, skipping the address bar.

## beforefocus Event

`beforefocus` event is dispatched _before_ the browser wants to focus an element. Most important
part is that `preventDefault()` of the `beforefocus` event should prevent the element from
receiving focus.

The event should have an important property to distinguish the reason for the focusing:

```ts
interface BeforeFocusEvent extends FocusEvent {
  reason: FOCUS_FORWARD | FOCUS_BACKWARD | PROGRAMMATIC | AUTOFOCUS | UI
}
```

`FOCUS_FORWARD` means that the element is about to be focused because `Tab` is pressed or
`document.focusNext()` is called with `direction` set to `FORWARD`.

`FOCUS_BACKWARD` means that the element is about to be focused because `Shift+Tab` is
pressed or `document.focusNext()` is called with `direction` set to `BACKWARD`.

`PROGRAMMATIC` means that the element is about to be focused because the application has
called `element.focus()`.

`AUTOFOCUS` means that the element is about to be focused because of the `autofocus` DOM
attribute.

`UI` means that the element is about to be focused because the user has clicked (or tapped)
an element.

It is important that calling `preventDefault()` actually prevents the element focus.

Some important expected behaviours:

* When you `Tab` into a page from the address bar, `beforefocus` should be dispatched for
   the element the browser wants to focus first and focusing it should be preventable with
   `preventDefault()`.
* When something inside an `<iframe>` is about to be focused, `beforefocus` should first be
   dispatched in the parent application that contains the iframe, the iframe element should
   be the target of that `beforefocus` event instance, allowing the parent application to
   `preventDefault()` the focus grab by the iframe. Next, if the parent application has not
   called `preventDefault()`, `beforefocus` event should be dispatched inside an iframe.
* When a user clicks an input (or any `tabindex=0` element), `beforefocus` for
   that input is dispatched (with the `reason=UI`) and if `preventDefault()` is called,
   the input shoudn't receive focus (effectively leaving `document.activeElement` set to
   `<body>` as if the click has happened on an empty space of the page).
* When screen readers want to move focus with virtual cursor, `reason` should be either
   `FOCUS_FORWARD` or `FOCUS_BACKWARD` (relative to currently focused element position).

## Usage Examples

### List Example

Consider the following example — a page containing a list of focusable items:
<img src="/images/beforefocus-example-1.png" alt="List example to describe beforefocus event usage" width="400" />

From the keyboard navigation perspective, it is a common practice that when you are tabbing
through the page, only one item of the list receives focus, next Tab moves you outside of the
list (otherwise it will take forever to tab through a very long or infinite virtualized list).
And to move between the list items, the arrow keys are normally used. It is also a common practice
to send focus to the selected item while tabbing through the page.

In the example above, tabbing from the address bar will move to `Focusable Item 1`, not to the
selected one.

First way for working this around is a technique called roving tabindex — you set `tabindex=-1`
to all list items but the selected one which has `tabindex=0`. To implement roving tabindex in a,
for example, React application, `tabindex` attribute updates need to be done either in the
React tree or as direct DOM manipulations on top of the React life cycle. Doing it in the
React tree requires rerendering of the entire list when the selected item changes, doing it as
direct DOM manipulations could lead to conflicts and inconsistencies with the components inside
the list item (they might legitimately be using/changing `tabindex` for their own purposes).

Things get even more complicated with roving tabindex when a list item contains multiple
focusable components inside — you have to go deep inside the item and make sure every focusable
element inside inactive item gets `tabindex` properly set/unset. Those could be the components
owned by different teams and various third party components. Imagine rich chat message with links
and buttons inside as an example of such a list item. And when your list item contains a cross
origin iframe, roving tabindex technique completely hits the wall.

A cherry on top with roving tabindex is the necessity to carefully maintain the state — if the
data layer decides that the currently selected item needs to be removed, you have to make sure
that the list is not left in a state when all items are `tabindex=-1` — this might be very
nontrivial in a complex application.

And of course, mainting roving tabindex is a continuous set of DOM updates which lead to
reflows, style recalculations and generally have a toll on the application performance.

After roving tabindex, our next resort is putting two invisible inputs before and after our list.
Their purpose is to receive focus when a user tabs through the page and immediately redirect
that focus to a selected item. This works in a lot of cases, but it also is hacky and far from
ideal. Those dummy inputs need to be inserted in the DOM and maintained properly. In a React
application example, they again need either to be a part of React tree or inserted at the proper
moment and into the proper place as direct DOM manipulations (making sure that they stay
first/last when React rerenders the tree).

Additionally, to prevent the screen readers from choking when the dummy input receives focus
and moves it right away, those dummy inputs need to be focusable elements with `aria-hidden=true`.
`axe-core` that is commonly used to validate the DOM state from the accessibility point, will
highlight those elements as violations and you will have to train your testing team to distinguish
those intentional violations from the actual violations.

Another complication is that even though those dummy inputs are invisible, you need to position them
properly to make sure that focusing them doesn't cause the scrollable containers to scroll in
unexpected ways.

And iframes might ruin the party again — you can call `iframe.focus()` but that won't focus the
proper element inside the iframe.

That is when `beforefocus` event might come into play.

```ts
listContainer.addEventListener('beforefocus', event => {
  ...

  // When we are moving through the page.
  if (event.reason === FOCUS_FORWARD || event.reason === FOCUS_BACKWARD) {
    if (
      !selectedItem?.contains(event.target) // If the browser is going to focus something other
                                            // than an element from the selected item,
      listContainer.contains(selectedItem) && // and the selectedItem is still in the DOM,
      !listContainer.contains(event.relatedTarget) // and we are moving into the list from outside of the list.
    ) {
      event.preventDefault(); // Don't focus the element the browser wanted to focus.

      document.focusNext({
         contextElement: selectedItem, // But do focus the selected item.
         includeContextElement: true
      });
    }
  }

  ...
});
```

### Unstolen Focus Example

`beforefocus` would also be useful for preventing unexpected focus movements in complex applications.

Imagine you have two tabs in the UI: Tab1 and Tab2. You render them at once, but Tab1 is currently active and
your focus is inside Tab1 contents. At the same time, you have an iframe inside Tab2 contents, it will become
visible when you activate Tab2. But the iframe is an embedded application which doesn't really know that it's
not active now. But it has autofocus or a programmatic focus on load. Once it loads, the focusing logic
inside that yet inactive iframe in Tab2 contents will steal the focus from whatever you currently have focused
in your active Tab1. With `beforefocus` you might do something like:

```ts
tabs.addEventListener('beforefocus', event => {
  // When an inactive tab claims focus, just tell it no.
  if (!activeTabContents.contains(event.target)) {
   event.preventDefault();
  }
});
```

## Summary

A combination of `document.focusNext()` and `beforefocus` event would allow to easily
implement various high level keyboard navigation abstractions — all kinds of modal dialogs,
lists, arrow navigation areas, groups of focusables — without fundamentally arguing about
how these abstractions should look like.

These changes are not breaking anything that already exists and don't seem very complex to
implement.
